<?php

/**
 * This file is part of Switcher.
 * 
 * Switcher is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Switcher is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Switcher. If not, see <http://www.gnu.org/licenses/>.
 * 
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 */

namespace switcher\utils;

/**
 * Utility class for reflection- and introspection-related operations.
 * 
 * @author Dri <switcher.framework@gmail.com>
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 * @package switcher\utils
 * @since 1.0.0
 */
final class ReflectionHelper
{
	/**
	 * Hidden constructor
	 */
	private function __construct()
	{
	}
	
	/**
	 * Find a property in the class hierarchy and return its reflection property.
	 * 
	 * @param \ReflectionClass $reflectionClass The base class to scan
	 * @param string $property The property to find
	 * @return \ReflectionProperty The reflection property of the class or any parent class
	 * @throws \ReflectionException If the property could not be found
	 */
	public static function getReflectionProperty(\ReflectionClass $reflectionClass, $property)
	{
		try
		{
			return $reflectionClass->getProperty($property);
		}
		catch (\ReflectionException $e)
		{
			$parentClass = $reflectionClass->getParentClass();
			
			if ($parentClass != null)
			{
				return self::getReflectionProperty($parentClass, $property);
			}
			
			throw $e;
		}
	}
	
	/**
	 * Set a new value to an object's property, even if the property is private
	 * or doesn't exist.
	 * 
	 * @param $object The object holding the property
	 * @param $property The property to set
	 * @param $value The new value of the property
	 * @throws \InvalidArgumentException If <i>object</i> is not an object
	 */
	public static function setProperty($object, $property, $value)
	{
		self::checkType($object, 'object');
		
		$reflectionClass = new \ReflectionClass($object);
		
		try
		{
			$reflectionProperty = self::getReflectionProperty($reflectionClass, $property);
			$reflectionProperty->setAccessible(true);
			$reflectionProperty->setValue($object, $value);
		}
		catch (\ReflectionException $e)
		{
			$object->$property = $value;
		}
	}
	
	/**
	 * Create an object and initialize its properties. The $properties array must contain
	 * one <i>@type</i> property with the class name of the object to create. The target
	 * class must have a no-arg constructor.
	 * 
	 * @param array $properties The poperties to initialize
	 * @return mixed An object with its properties initialized
	 * @throws \InvalidArgumentException If <i>$properties</i> is not an array
	 * @throws \InvalidArgumentException If <i>$properties</i> doesn't declare the class name
	 * @throws \ReflectionException If the class could not be found
	 */
	public static function createObject(array $properties)
	{
		self::checkType($properties, 'array');
		
		if ( !array_key_exists('@type', $properties) )
		{
			throw new \InvalidArgumentException('Type Not Found');
		}
		
		$reflectionClass = new \ReflectionClass($properties['@type']);
		$object = $reflectionClass->newInstance();
		
		self::overrideObject($object, $properties);
		
		return $object;
	}
	
	/**
	 * Override an object's properties with new values.
	 * 
	 * @param $object The object to override
	 * @param $properties An associative array of properties
	 * @throws \InvalidArgumentException If <i>object</i> is not an object
	 * @throws \InvalidArgumentException If <i>$properties</i> is not an array
	 */
	public static function overrideObject($object, array $properties)
	{
		self::checkType($object, 'object');
		self::checkType($properties, 'array');
		
		$reflectionClass = new \ReflectionClass($object);
		
		foreach ($properties as $property => $value)
		{
			try
			{
				$reflectionProperty = self::getReflectionProperty($reflectionClass, $property);
				
				if ( !$reflectionProperty->isPrivate() )
				{
					$reflectionProperty->setAccessible(true);
					$reflectionProperty->setValue($object, $value);
				}
			}
			catch (\ReflectionException $e)
			{
				$object->$property = $value;
			}
		}
	}
	
	/**
	 * Create recursively an associative array. This function allows to create an array of
	 * properties compatible with <b>overrideObject</b>. It works with both an <i>array</i>,
	 * a <i>string</i> or a <i>SimpleXMLElement</i>.
	 * 
	 * @param mixed $element The value to turn into an array of properties
	 * @return array An associative array representing an object's properties
	 */
	public static function createProperties($element)
	{
		if ( is_array($element) )
		{
			return $element;
		}
			
		if (is_string($element) || $element->count() == 0)
		{
			return (string) $element;
		}
		
		$array = (array) $element;
		
		foreach ($array as $property => $value)
		{
			$array[$property] = self::createProperties($value);
		}
		
		return $array;
	}
	
	/**
	 * Check if an item is of the right type.
	 * 
	 * @param $item The item to check
	 * @param $type The expected type of the item
	 * @throws \InvalidArgumentException If the item is not of the expected type
	 */
	private static function checkType(&$item, $type)
	{
		if (gettype($item) != $type)
		{
			throw new \InvalidArgumentException('Not of type ' . $type . ' : ' . (string) $item);
		}
	}
}

?>